#include "Matrix.h"
#include "MatlabPrinter.h"
#include <iostream>
#include <fstream>
#include <string>
#include <exception>
#include <cmath>
#include <sstream>
#include <cassert>

	/***************************************************************************/
	//                 Implementation of OurMatrix Class                       //
	/***************************************************************************/

// Constructor (Default)
Matrix::Matrix():numRows(0),numCols(0){
	mat = new Elements();
	name = "Default";
}

// Constructor (Takes a file name and parses the input)
Matrix::Matrix(std::string input){
	mat = new Elements();
    *mat = ParseFile(input);
	name = input.erase(input.size()-4, input.size());
	MatlabPrinter::inputMatrix(*mat, input);
}

// Constructor (Creates an empty matrix of size m by n)
Matrix::Matrix(int rows, int columns){
	mat = new Elements();
	*mat = zeroMatrix(rows, columns);
	name = "SizeBased";
}

// Constructor (Created a matrix based on the input)
Matrix::Matrix(Elements input){
	mat = new Elements();
    for (unsigned int i = 0; i < input.at(0).size(); i++){
        mat->push_back(std::vector < double> ());
        for (unsigned int j = 0; j < input.size(); j++)
            mat->at(i).push_back(input.at(i).at(j));
    }
	name = "Element based";
}

// Destructor
Matrix::~Matrix(){
	if (mat != NULL){
		delete mat;
		mat = NULL;
	}
}

// Copy Constructor
Matrix::Matrix( const Matrix& other ){
	if (this == &other)
		return;
	mat = new Elements( *other.mat );
	numRows = other.numRows;
	numCols = other.numCols;
	name = other.name;
}

// Assignment Operator
Matrix& Matrix::operator=( const Matrix& other ){
	if (this == &other)
		return *this;
	if (mat != NULL)
		delete mat;
	mat = new Elements( *other.mat );
	numRows = other.numRows;
	numCols = other.numCols;
	name = other.name;
	return *this;
}


	/****************************************************************/
	//               Overloading of OurMatrix Class                 //
	/****************************************************************/

//	() operator; acts as the [][] operator
double& Matrix::operator()(const int nRow, const int nCol){  
    assert( nRow < this->numRows && nRow >= 0 );
    assert( nCol < this->numCols && nCol >= 0 );
    return (*mat)[nRow][nCol];
} 

//	<< operator
std::ostream& operator<<(std::ostream& out, const Matrix rhs){	    
	out.precision(3);
	out.setf(std::ios::fixed,std::ios::floatfield);	
    for (int i = 0; i < rhs.numRows; i++){
        for (int j = 0; j < rhs.numCols; j++){
            out << rhs.mat->at(i).at(j) << "\t";
        }
        out << std::endl;    
    }   
    return out;   
}

// Scalar multiply Matrix * double
Matrix operator*(Matrix &mat, double Scal){
	MatlabPrinter::scalarMultiply(Scal, mat);
	Matrix result(mat.numRows, mat.numCols);
	for(int i = 0; i<mat.numRows; i++){
		for(int j = 0; j<mat.numCols; j++){
			result(i, j) = Scal*mat(i,j);
		}
	}
	return result;
}

// Scalar multiply double*Matrix
Matrix operator*(double Scal, Matrix &mat){
	MatlabPrinter::scalarMultiply(Scal, mat);
	Matrix result(mat.numRows, mat.numCols);
	for(int i = 0; i<mat.numRows; i++){
		for(int j = 0; j<mat.numCols; j++){
			result(i, j) = Scal*mat(i,j);
		}
	}
	return result;
}

// Matrix Multiply
Matrix Matrix::operator*(const Matrix &rhs){
	MatlabPrinter::matrixMultiply(*this, rhs);
	if(numCols==rhs.numRows){
		Matrix result(numRows, rhs.numCols);		
		for(int i=0; i < numRows; i++){
			for(int j=0; j< rhs.numCols; j++){
				for(int k=0; k < numCols; k++){
					result(i,j) += (*this)(i,k)*rhs.mat->at(k).at(j);
				}
			}
		}
		return result;
	}
	else{
        std::stringstream out;
		out<<"Unable to mulitply a "<<numRows<<"x"<<numCols<<" Matrix by a "<<rhs.numRows<<"x"<<rhs.numCols<<" Matrix.";
		throw out.str();
	}
}

// Matrix Power
Matrix Matrix::Mpow(int power){
	//check square
	MatlabPrinter::matrixPower(*this, power);
	if(numRows==numCols){
		Matrix result(numRows, numCols);
		result = *this;
		
		if(power < -1){
			result = this->inverse();
			for(int i=power; i < 0; i++){
				result = result*(this->inverse());
			}
			return result;
		}
		if(power == -1){
			result = this->inverse();
			return result;
		}		
		if(power == 0){
			result  = identity(numRows);
			return result;
		}
		if(power == 1){
			result = *this;
		}
		//Recursive itteration ot find power n>1
		for(int i=1; i< power; i++){
			result = result*(*this);
		}
		return result;
	}
	else{
        std::stringstream out;
		out << "ERROR: Matrix size is: "<< numRows <<"x"<<numCols<<"; In order to compute matrix to the "<<power<<" power the matrix must be square.\n";
		throw out.str();
	}
}

// + operator to add two matrices
Matrix operator+(Matrix &lhs, Matrix &rhs){
    //Make sure they are the same size
    assert( lhs.numRows == rhs.numRows );
    assert( lhs.numCols == rhs.numCols );
	MatlabPrinter::matrixAddition(lhs, rhs);
	Matrix sum(lhs.numRows, rhs.numRows);
	for( int i = 0; i < lhs.numRows; i++){
        for( int j = 0; j < lhs.numCols; j++){
            sum(i , j) = lhs(i , j) + rhs(i , j);
        }
    }
    return sum;
}

// - operator to subtract two matricies
Matrix operator-(Matrix &lhs, Matrix &rhs){
    //Make sure they are the same size
    assert( lhs.numRows == rhs.numRows );
    assert( lhs.numCols == rhs.numCols );
	MatlabPrinter::matrixSubtraction(lhs, rhs);
	Matrix difference(lhs.numRows, rhs.numRows);
	for( int i = 0; i < lhs.numRows; i++){
        for( int j = 0; j < lhs.numCols; j++){
            difference(i , j) = lhs(i , j) - rhs(i , j);
        }
    }
	return difference;
}

// - operator to take the negative of a matrix
Matrix Matrix::operator-(){
	MatlabPrinter::unaryMinus(*this);
	return -1*(*this);
}

// == operator to check to see if two matrices are equal
bool operator==(Matrix &lhs, Matrix &rhs){
    //Make sure they are the same size
    assert( lhs.numRows == rhs.numRows );
    assert( lhs.numCols == rhs.numCols );
	MatlabPrinter::equalityCheck(lhs, rhs);
    for( int i = 0; i < lhs.numRows; i++){
        for( int j = 0; j < lhs.numCols; j++){
            if( lhs(i, j) != rhs(i, j)){
                return false;
            }
        }
    }

    return true;
}

// != operator to check to see if two matrices are not equal
bool operator!=(Matrix &lhs, Matrix &rhs){
	MatlabPrinter::inequalityCheck(lhs, rhs);
    return !(lhs == rhs);
}

// zeroMatrix function will return the a mxn matriz full of zeros
		// Protected function
Elements Matrix::zeroMatrix(int m,int n){
	Elements result;
	for (int i = 0; i < m; i++){
		result.push_back(std::vector<double>());
		for (int j = 0; j < n; j++){
			result[i].push_back(0);
		}
	}
	// Instantiate the size values
	numCols = result[0].size();             
	numRows = result.size();
	return result;
}

// Identity function will create an identity matrix of size size
		// Use:  identy_matrix = Matrix::identity_matrix(size)
Matrix Matrix::identity(int size){
	Matrix result(size, size);
	MatlabPrinter::identity(size);
	for (int i = 0; i < size; i++){
		for (int j = 0; j < size; j++){
			if (i == j)
				result(i,j) = 1;
			else
				result(i,j) = 0;
		}
	}
	return result;
}

// ParseFile function will returns a vector of vectors; elements
		// Protected function
Elements Matrix::ParseFile(std::string filename){
    Elements result;                            // Declare local vars
    std::ifstream inFile;
    double input;
	char next;
    int index=0;    	    
    inFile.open(filename.c_str());              // Open the file with the desired file name
	if (inFile.is_open()){
		result.push_back(std::vector<double>());// Add a row to our 2d vector
		while(!inFile.eof()){		
			inFile >> input;                    // Get input
			result[index].push_back(input);     // add the input to our current row
			inFile.get(next);
			if(next=='\r'||next =='\n'){      // add a new row if we get a newline char
				index++;
				result.push_back(std::vector<double>());
			}
		}		
		numCols = result[0].size();             // Instantiate the size values
		numRows = result.size();
		return result;
	}
	else {
        std::cout << "FNF. Please check your directories." << std::endl;
        std::cout << "The input file needs to be in the root of your build directory." << std::endl;
		exit(EXIT_FAILURE);
	}
}

// addRowMultiple function will modify the matrix its called on
		// Protected function : row1 = row1 + scale * row2
void Matrix::addRowMultiple(int row1, int row2, double scale){
	for (int i = 0; i < this->getNumCols(); i++){
		(*this)(row1, i) += scale*(*this)(row2, i);
	}
}
